Pythonのテストフレームワーク unittest を使ってみよう
unittest について
Python の標準ライブラリの unittestモジュールは、テストフレームワークです。
組み込み関数の sum() は、与えたリスとなどのイテラブルオブジェクトを合計した値を返します。
code: python
sum([1,2,3]) の結果が6になるかどうかをテストするためのコードは次のものです。
code: python
In 1: assert sum(1,2,3) == 6, "Expecting 6" assert文は与えた式が真のときは何もしません。偽のときは、AssertionErrorの例外を発行します。assert文のメッセージは省略しても構いません。
code: python
In 2: assert sum(0,1,2) == 6, "Expecting 6" ---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-2-1ba3b8f3759d> in <module>
----> 1 assert sum(0,1,2) == 6, "Expecting 6" AssertionError: Expecting 6
Python の REPL でテストする代わりに、スクリプトにしてみましょう。
code: s01_test.py
def test_sum():
assert sum(1,2,3) == 6, "Expecting 6" if __name__ == '__main__':
test_sum()
print('All testcase passed.')
この例の、test_fibo()はテストケースと呼ばれます。これを実行してみましょう。
code:bash
$ python s01_test.py
All testcase Passed.
テストが成功したので、"All testcase passed."が表示されました。
次に、意図的に失敗するテストを書いてみます。
code: s01_test.pyp
def test_sum1():
assert sum(1,2,3) == 6, "Expecting 6" def test_sum2():
assert sum(0,1,2) == 6, "Expecting 6" def test_sum3():
assert sum((1,2,3)) == 6, "Expecting 6"
if __name__ == '__main__':
test_sum1()
test_sum2()
test_sum3()
print('All testcase passed.')
code: bash
% python s02_test.py
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s02_test.py", line 9, in <module>
test_sum2()
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s02_test.py", line 5, in test_sum2
assert sum(0,1,2) == 6, "Expecting 6" AssertionError: Expecting 6
テストケース test_sum2() でAssertionErrorが発行されたことがわかります。
コードの誤りをエラーの場所と期待される結果とともに確認することができます。
assert文でテスト結果を評価しているため、test_sum3()が実行されていないことに注目してください。
テストランナー
この方法でテストを作成する場合、簡単なチェックでは問題ありませんが、テストケースが多いときや、複数のテストが失敗する場合はすべてのテストケースを実施するのにも時間がかかりますし、管理が難しくなります。こうしたときに、テストランナーが必要になります。テストランナーは、テストの実行、出力の確認、およびテストとアプリケーションのデバッグと診断のためのツールを提供するために設計された特別なアプリケーションです。
unittest はテストフレームワークとテストランナーが提供されます。
前述のテストを unittest のテストケースとするためには、次のことを行う必要があります。
unittest をインポート
TestCase クラスを継承した TestSumクラスを作成
テスト関数をTestSumクラスのメソッドに変換。メソッド名は testで始まることが必要
assert文に代えてTestCaseクラスのassertEqual()を使用するように修正
unittest.main()を呼び出すように、コマンドラインでのエントリポイントを変更
code: s03_unittest.py
import unittest
class TestSum(unittest.TestCase):
def test_sum1(self):
self.assertEqual(sum(1, 2, 3), 6, "Expecting 6") def test_sum2(self):
self.assertEqual(sum(0, 1, 2), 6, "Expecting 6") def test_sum3(self):
self.assertEqual(sum((1, 2, 3)), 6, "Expecting 6")
if __name__ == '__main__':
unittest.main()
unittest のテストランナーは testで始まるメソッド名を検索します。
これを実行すると次のようになります。
code:bash
% python s03_unittest.py
.F.
======================================================================
FAIL: test_sum2 (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s03_unittest.py", line 10, in test_sum2
self.assertEqual(sum(0, 1, 2), 6, "Expecting 6") AssertionError: 3 != 6 : Expecting 6
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
こんどは3つのテストケースが実行されていること、test_sum2()が失敗したことがわかります。
注意:テストケースが実施される順序は、定義順ではありません。テストケースのメソッド名を文字列としてソートされた順序で実施されます。
unittest で一般的に使われるアサートメソッドには次のものがあります。すべてのアサートメソッドについては unittestのドキュメント を参照してください。 table: unittestのアサートメソッド
ソッド 確認事項
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)
コマンドラインオプション
table: unittestのコマンドラインオプション
オプション 説明
-h / --help ヘルプメッセージの出力
-v / --verbose 冗長表示を有効にする
-q / --quiet エラー以外はなにも出力しない
-f / --failfast 最初に失敗したテストで終了する
-k PATTERN PATTERNにマッチするテストケースやテストを実施する
-c / --catch テストが終わるまで Ctrl-C を捕獲し、終了後に結果を表示する
-b / --buffer テスト中は stdout と stderr をバッファリングする
--locals トレースバック内でローカル変数を表示する
テストコードの構成
testCaseクラスには、数多くのテストが記述されることになります。今回のテストは単純なものですが、何かしらの前処理./後処理が必要になることもあります。例えば、データベース接続や一時ファイル、その他のリソースを初期化する必要があるといったものです。これらはフィクスチャ(Fixture)と呼ばれます。TestCase には、テストで必要となるフィクスチャを設定したりクリーンアップしたりするための特別な フック(Hock) があります。フィクスチャを設定するには、setUp() をオーバーライドします。後始末をするには、 tearDown() をオーバーライドします。
TestCaseのインスタンスは、個々のテストケースを実施するために使用される一意のテストフィクスチャとして作成されます。このため、__init__()、setUp()、tearDown()などのメソッドは、定義しておくと、テストごとに呼び出されます。このことは重要で、Python 3.8 から使用できるようになったIsolatedAsyncioTestCaseでは非同期処理でテストを並列に実施するため、共有フィクスチャに実装には注意する必要があります。
以下の例では、モジュール mymath に定義した関数をテストするために、TestCase クラスに 2 つのテストを記述しています。setup() メソッドは、各テストの shortDescription() に基づいて引数を初期化し、 teardown() メソッドは各テストの終了時に実行されます。
code: mymath.py
def add(x, y):
"""
>> add(10, 20)
30
"""
return x + y
def multiply(x, y):
"""
>> multiply(5, 6)
30
"""
return x * y
def square(n):
"""
>> square(5)
25
"""
return(n**2)
def cube(n):
"""
>> cube(5)
125
"""
return(n**3)
code: s05_setup_trardown.py
import unittest
from mymath import add, multiply, square, cube
class MyMathTest(unittest.TestCase):
test_data = {
'Add': {'a':10, 'b': 20, 'expect': 30},
'Multiply': {'a':5, 'b': 6, 'expect': 30},
'Square': {'a':5, 'b': None, 'expect': 25},
}
def setUp(self):
self.a = 0
self.b = 0
name = self.shortDescription()
print(f'\nSetup for {name}: {self.a}, {self.b}')
def tearDown(self):
print(f'\nEnd of test {self.shortDescription()}')
def testAdd(self):
"""Add"""
result = add(self.a, self.b)
def testMultiply(self):
"""Multiply"""
result = multiply(self.a, self.b)
def testSquare(self):
"""Square"""
result = square(self.a)
if __name__ == '__main__':
unittest.main()
shortDescription()は、テストの説明を一行分、または説明がない場合には None を返します。デフォルトでは、テストメソッドの docstring の先頭の一行、または None を返します。
TestCaseクラスにはフィクスチャに関連するメソッドには、次のものがあります。
setUp()
テストフィクスチャを準備するために呼び出されるメソッド。 これは、テストメソッドを呼び出す直前に呼び出されます。 AssertionError またはスキップされるテスト(後述)を除き、このメソッドによって発生した例外は、テストの失敗ではなくエラーと見なされます。 デフォルトの実装は何もしません。
tearDown()
テストメソッドが呼び出され、結果が記録された直後に呼び出されるメソッド。 これは、テストメソッドで例外が発生した場合でも呼び出されるため、サブクラスでの実装では、内部状態のチェックに特に注意する必要があります。 このメソッドによって発生したAssertionError またはスキップされるテスト(後述)以外の例外は、テストの失敗ではなく追加のエラーと見なされます(レポートされるエラーの総数が増加します)。 このメソッドは、テストメソッドの結果に関係なく、setUp() が成功した場合にのみ呼び出されます。 デフォルトの実装は何もしません。
addCleanup(function, /, *args, **kwargs)
テスト中に使用されたリソースをクリーンアップするために、tearDown() の後に呼び出される関数を追加します。 関数は、追加された順序とは逆の順序(LIFO:Last In First Out) で呼び出されます。 これらは、追加されたときにaddCleanup() に渡される引数とキーワード引数を使用して呼び出されます。
setUp() が失敗した場合、つまり tearDown() が呼び出されなかった場合でも、追加されたクリーンアップ関数は呼び出されます。
doCleanups()
このメソッドは、tearDown() の後、または setUp() が例外を発生させた場合は、setUp() の後に無条件に呼び出されます。
addCleanup() によって追加されたすべてのクリーンアップ関数を呼び出す必要があります。 tearDown() の前にクリーンアップ関数を呼び出す必要がある場合は、doCleanups() を自分で呼び出すことができます。
クラスフィクスチャ
TestCase クラスには setUpClass() メソッドがあり、 これをオーバーライドすることで、TestCase クラス内の個々のテストを実行する前に実行することができます。同様に、tearDownClass() メソッドは、クラス内のすべてのテストの後に実行されます。これらのメソッドはどちらもクラスメソッドです。そのため、@classmethod ディレクティブで装飾する必要があります。
次のコードは、クラスフィクスチャの動作を確認するためのものです。
code: s06_class_fixture.py
import unittest
from mymath import add, multiply, square, cube
class MyMathFixtureTest(unittest.TestCase):
test_data = {
'Add': {'a':10, 'b': 20, 'expect': 30},
'Multiply': {'a':5, 'b': 6, 'expect': 30},
'Square': {'a':5, 'b': None, 'expect': 25},
}
@classmethod
def setUpClass(cls):
print(f'\ncalled once before any tests in {cls.__name__}')
@classmethod
def tearDownClass(cls):
print(f'\ncalled once after all tests in {cls.__name__}')
def setUp(self):
self.a = 0
self.b = 0
name = self.shortDescription()
print(f'\nSetup for {name}: {self.a}, {self.b}')
def tearDown(self):
print(f'\nEnd of test {self.shortDescription()}')
def testAdd(self):
"""Add"""
result = add(self.a, self.b)
def testMultiply(self):
"""Multiply"""
result = multiply(self.a, self.b)
def testSquare(self):
"""Square"""
result = square(self.a)
if __name__ == '__main__':
unittest.main()
code: bash
% python s06_class_fixture.py
called once before any tests in MyMathFixtureTest
Setup for Add: 10, 20
End of test Add
.
Setup for Multiply: 5, 6
End of test Multiply
.
Setup for Square: 5, None
End of test Square
.
called once after all tests in MyMathFixtureTest
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
TestCaseクラスにはクラスフィクスチャに関連するメソッドには、次のものがあります。
classmethod setUpClass()
個別のクラス内のテストが実行される前に呼び出されるクラスメソッドです。 setUpClass() はクラスを唯一の引数として受け取ります。
classmethod tearDownClass()
個別のクラス内のテストが実行された後に呼び出されるクラスメソッドです。 tearDownClass() はクラスを唯一の引数として受け取ります。
classmethod addClassCleanup(function, /, *args, **kwargs)
tearDownClass() の後に呼び出される関数を追加します。この関数はリソースのクリーンアップのために使用します。追加された関数は、追加された順序と逆の順番(LIFO)で呼び出されます。 addClassCleanup() に渡された引数とキーワード引数が追加された関数にも渡されます。
setUpClass() が失敗した場合、つまり tearDownClass() が呼ばれなかった場合でも、追加されたクリーンアップ関数は呼び出されます。
classmethod doClassCleanups()
このメソッドは、 tearDownClass() の後、もしくは、 setUpClass() が例外を投げた場合は setUpClass() の後に、無条件で呼ばれます。
このメソッドは、 addClassCleanup() で追加された関数を呼び出す責務を担います。もし、クリーンアップ関数を tearDownClass() より前に呼び出す必要がある場合には、 doClassCleanups() を明示的に呼び出してください。
doClassCleanups() は、どこで呼び出されても、クリーンアップ関数をスタックから削除して実行します。
サブテスト
テストの間に非常に小さな違いがある場合 (たとえばいくつかのパラメータなど)、 unittest では subTest() コンテキストマネージャーを使用してテストメソッドの内部でそれらを区別することができます。
code: s07_subtest.py
import unittest
class NumbersTest(unittest.TestCase):
def test_even(self):
"""
Test that numbers between 0 and 5 are all even.
"""
for i in range(0, 4):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)
if __name__ == '__main__':
unittest.main()
code: bash
$ python s07_subtest.py
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
Test that numbers between 0 and 5 are all even.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s07_subtest.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
Test that numbers between 0 and 5 are all even.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s07_subtest.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=2)
もしsubTest()を使用せずに次のように書いている、最初のテストで失敗してしまいます。そのままでは、iの値が出力されないため、原因を把握するのに時間がかかるかもしれません。
code: s08_no_subtest.py
import unittest
class NumbersTest(unittest.TestCase):
def test_even(self):
"""
Test that numbers between 0 and 5 are all even.
"""
for i in range(0, 4):
print(f'i={i}')
self.assertEqual(i % 2, 0)
if __name__ == '__main__':
unittest.main()
code: bash
% python s08_no_subtest.py
i=0
i=1
F
======================================================================
FAIL: test_even (__main__.NumbersTest)
Test that numbers between 0 and 5 are all even.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s08_no_subtest.py", line 11, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
テスト作成時にi の値を表示するようにしたとしても、subTest()を使った方がスッキリしています。
スキップと予測される失敗
@unittest.expectedFailureデコレータを使用すると、テストを予想される失敗またはエラーとしてマークします。 テストが失敗した場合、またはテスト関数自体にエラーが発生した場合は、テストが成功したと見なされます。 逆テストにパスした場合、失敗と見なされます。
code: s09_expectedfailure.py
import unittest
import requests
class DemoTest(unittest.TestCase):
status = 200
def setUp(self):
@unittest.skip('無条件にスキップ')
def test_request1(self):
r1 = requests.get(self.url)
@unittest.skipIf(status > 200, 'status が 200より大きい時はスキップ')
def test_request2(self):
# アサーションの結果が真であれば続行し、
# そうでなければテストをスキップします。
r2 = requests.get(self.url)
status2 = r2.status_code
self.assertTrue(status2 > self.status)
@unittest.skipUnless(status == 404, 'status が 400 でなければスキップ')
def test_request3(self):
# 結果が真でない限り、このテストをスキップします。
r3 = requests.get(self.url)
status3 = r3.status_code
self.assertTrue(status3 > self.status)
@unittest.expectedFailure
def test_request4(self):
# テストケースは "expected failed"(予想される失敗)と表示されます。
# テストが実行された場合、テスト結果は失敗ではないと判断します。
r4 = requests.get(self.url+'/test4')
status4 = r4.status_code
self.assertTrue(status4 ==self.status)
def tearDown(self):
pass
if __name__ == '__main__':
unittest.main()
code: bash
$ python s09_expectedfailure.py
sFsx
======================================================================
FAIL: test_request2 (__main__.DemoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s09_expectedfailure.py", line 19, in test_request2
self.assertTrue(status2 > self.status)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 4 tests in 2.152s
FAILED (failures=1, skipped=2, expected failures=1)
code: bash
% python s09_expectedfailure.py -v
test_request1 (__main__.DemoTest) ... skipped '無条件にスキップ'
test_request2 (__main__.DemoTest) ... FAIL
test_request3 (__main__.DemoTest) ... skipped 'status が 404 でなければスキップ'
test_request4 (__main__.DemoTest) ... expected failure
======================================================================
FAIL: test_request2 (__main__.DemoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/02_Unittest/s09_expectedfailure.py", line 19, in test_request2
self.assertTrue(status2 > self.status)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 4 tests in 2.212s
FAILED (failures=1, skipped=2, expected failures=1)
@unittest.skip(reason)
デコレートしたテストを無条件でスキップします。reason にはテストをスキップした理由を記載します。
@unittest.skipIf(condition, reason)
condition が真の場合、デコレートしたテストをスキップします。
@unittest.skipUnless(condition, reason)
condition が偽の場合、デコレートしたテストをスキップします。
予想される失敗の処理
次のような、sum()をテストするとき、1つの整数や文字列のような悪い値を与えたらどうなるでしょうか?
この場合、sum()はエラーを投げることになるでしょう。エラーが発生すると、テストが失敗してしまいます。
予想されるエラーを処理する特別な方法があります。.assertRaises() をコンテキストマネージャーとして使用し、withブロック内でテストステップを実行することができます。
code: test_21_assertRaise.py
import unittest
from fraction import Fraction
def sum(arg):
total = 0
for val in arg:
total += val
return total
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
整数のリストを加算できるかどうかのテスト
"""
result = sum(data)
self.assertEqual(result, 6)
def test_list_fraction(self):
"""
分数のリストをまとめることができることをテスト
"""
result = sum(data)
self.assertEqual(result, 1)
def test_bad_type(self):
"""
文字列を入力に与えたテスト
"""
data = "banana"
with self.assertRaises(TypeError):
result = sum(data)
if __name__ == '__main__':
unittest.main()
このテストケースは、sum(data) が TypeError を発生させた場合にのみ通過するようになりました。TypeError は任意の例外タイプで置き換えることができます。
TestSuite クラス
Python のテストフレームワークは、テストケースのインスタンスを、テストする機能別にグループ化することができる便利なメカニズムを提供します。このメカニズムは unittest モジュールの TestSuite クラスによって利用可能になります。
code: s10_testsuite.py
import unittest
from s06_class_fixture import MyMathFixtureTest
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MyMathFixtureTest))
suite.addTest(MyMathFixtureTest('testMultiply'))
suite.addTest(MyMathFixtureTest('testSquare'))
suite.addTest(MyMathFixtureTest('testAdd'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
test_suite = suite()
runner.run(test_suite)
code: bash
% python s10_testsuite.py
called once before any tests in MyMathFixtureTest
Setup for Add: 10, 20
End of test Add
.
Setup for Multiply: 5, 6
End of test Multiply
.
Setup for Square: 5, None
End of test Square
.
Setup for Multiply: 5, 6
End of test Multiply
.
Setup for Square: 5, None
End of test Square
.
Setup for Add: 10, 20
End of test Add
.
called once after all tests in MyMathFixtureTest
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
TestSuiteクラスでテストスイートの構築をカスタマイズすると、複数のテストケースから類似するテストをグループ化したり、テストの順序なども変更することができます。
テストのロードと起動
unittest には、クラスやモジュールからテストスイートを作成するための TestLoader クラスがあります。デフォルトでは、unittest.main(0)が呼ばれたときに unittest.defaultTestLoader インスタンスが自動的に作成されます。しかし、明示的にインスタンスを作成することで、特定のプロパティをカスタマイズすることができます。
code: s11_testloader.py
import unittest
from s06_class_fixture import MyMathFixtureTest
testLoad = unittest.TestLoader()
TestList = []
for testCase in testList:
testSuite = testLoad.loadTestsFromTestCase(testCase)
TestList.append(testSuite)
newSuite = unittest.TestSuite(TestList)
runner = unittest.TextTestRunner()
runner.run(newSuite)
loadTestsFromTestCase(testCaseClass)
TestCase の派生クラス testCaseClass に含まれる全テストケースのスイートを返します。
getTestCaseNames() で指定されたメソッドに対し、テストケースインスタンスが作成されます。 デフォルトでは test で始まる名前のメソッド群です。 getTestCaseNames() がメソッド名を返さなかったが、 runTest() メソッドが実装されている場合は、そのメソッドに対するテストケースが代わりに作成されます。
loadTestsFromModule(module, pattern=None)
指定したモジュールに含まれる全テストケースのスイートを返します。このメソッドは module 内の TestCase の派生クラスを検索し、見つかったクラスのテストメソッドごとにクラスのインスタンスを作成します。
モジュールが load_tests 関数を用意している場合、この関数がテストの読み込みに使われます。 これによりテストの読み込み処理がカスタマイズできます。 これが load_tests プロトコル です。 pattern 引数は load_tests に第3引数として渡されます。
loadTestsFromName(name, module=None)
文字列で指定される全テストケースを含むスイートを返します。
name には ドット表記で、モジュールかテストケースクラス、テストケースクラス内のメソッド、 TestSuite インスタンスまたは TestCase か TestSuite のインスタンスを返す呼び出し可能オブジェクトを指定します。このチェックはここで挙げた順番に行なわれます。すなわち、候補テストケースクラス内のメソッドは「呼び出し可能オブジェクト」としてではなく「テストケースクラス内のメソッド」として拾い出されます。
例えば SampleTests モジュールに TestCase から派生した SampleTestCase クラスがあり、 SampleTestCase にはテストメソッド test_one() ・ test_two() ・ test_three() があるとします。この場合、 name に 'SampleTests.SampleTestCase' と指定すると、 SampleTestCase の三つのテストメソッドを実行するテストスイートが作成されます。 'SampleTests.SampleTestCase.test_two' と指定すれば、test_two() だけを実行するテストスイートが作成されます。インポートされていないモジュールやパッケージ名を含んだ名前を指定した場合は自動的にインポートされます。
また、module を指定した場合、module 内の name を取得します。
loadTestsFromNames(names, module=None)
loadTestsFromName() と同じですが、名前を一つだけ指定するのではなく、複数の名前のシーケンスを指定する事ができます。戻り値は names 中の名前で指定されるテスト全てを含むテストスイートです。
getTestCaseNames(testCaseClass)
testCaseClass 中の全てのメソッド名を含むソート済みシーケンスを返します。 testCaseClass は TestCase のサブクラスでなければなりません。
discover(start_dir, pattern='test*.py', top_level_dir=None)
指定された開始ディレクトリからサブディレクトリに再帰することですべてのテストモジュールを検索し、それらを含む TestSuite オブジェクトを返します。pattern にマッチしたテストファイルだけがロードの対象になります。 (シェルスタイルのパターンマッチングが使われます)。その中で、インポート可能なもジュール (つまり Python の識別子として有効であるということです) がロードされます。
すべてのテストモジュールはプロジェクトのトップレベルからインポート可能である必要があります。開始ディレクトリがトップレベルディレクトリでない場合は、トップレベルディレクトリを個別に指定しなければなりません。
シンタックスエラーなどでモジュールのインポートに失敗した場合、エラーが記録され、ディスカバリ自体は続けられます。 import の失敗が SkipTest 例外が発生したためだった場合は、そのモジュールはエラーではなく skip として記録されます。
パッケージ (__init__.py という名前のファイルがあるディレクトリ) が見付かった場合、そのパッケージに load_tests() 関数があるかをチェックします。 関数があった場合、次に package.load_tests(loader, tests, pattern) が呼ばれます。 テストの検索の実行では、たとえ load_tests() 関数自身が loader.discover() を呼んだとしても、パッケージのチェックは1回のみとなることが保証されています。
コードとテストの分離
テストケースやテストコードの定義をテスト対象コードと同じモジュール(例:mymath.py) に置くことが出来ますが、テストコードを 独立したモジュール(例:test_mymath.py)に置くことを推奨します。
これには以下のような利点があるためです。
テストモジュールだけをコマンドラインから独立に実行することができる。
リリースするコードとテストコードをより簡単に分ける事ができる。
余程のことがない限り、テスト対象のコードに合わせてテストコードを変更することになりにくい。
テストコードは、テスト対象コードほど頻繁に変更されない。
テストコードをより簡単にリファクタリングすることができる。
C言語で実装したモジュールのテストは独立したモジュールになる。それなら同じにすることもなり。
テストの方策を変更した場合でも、ソースコードを変更する必要がない。
参考
Python 公式ドキュメント